<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title></title>
    <style type="text/css">
        input[type="text"] { width: 300px; }
    </style>
    <script type="text/javascript">
        window.addEventListener('load', function() {
            let clientID;
            let unsubscribed = false;
            let reconnect = true;
            let numFailures = 0;

            const utf8decoder = new TextDecoder();

            function handleErrors(response) {
                if (!response.ok) throw new Error(response.status);
                return response;
            }

            function FetchEventTarget(url, options) {
                const eventTarget = new EventTarget();
                // fetch with connection timeout maybe? https://github.com/github/fetch/issues/175
                fetch(url, options)
                    .then(handleErrors)
                    .then(response => {
                        eventTarget.dispatchEvent(new Event('open'));
                        let streamBuf = '';
                        let streamPos = 0;
                        const reader = response.body.getReader();
                        return new ReadableStream({
                            start(controller) {
                                function pump() {
                                    return reader.read().then(({ done, value }) => {
                                        // When no more data needs to be consumed, close the stream
                                        if (done) {
                                            eventTarget.dispatchEvent(new CloseEvent('close'));
                                            controller.close();
                                            return;
                                        }
                                        streamBuf += utf8decoder.decode(value);
                                        while (streamPos < streamBuf.length) {
                                            if (streamBuf[streamPos] === '\n') {
                                                const line = streamBuf.substring(0, streamPos)
                                                eventTarget.dispatchEvent(new MessageEvent('message', { data: JSON.parse(line) }))
                                                streamBuf = streamBuf.substring(streamPos + 1)
                                                streamPos = 0
                                            } else {
                                                ++streamPos
                                            }
                                        }
                                        pump();
                                    });
                                }
                                return pump();
                            }
                        });
                    })
                    .catch(error => {
                        eventTarget.dispatchEvent(new CustomEvent('error', { detail: error }));
                    })
                return eventTarget;
            }

            function connect() {
                reconnect = true;
                const abortController = new AbortController();

                const eventTarget = new FetchEventTarget(
                    window.location.protocol + '//' + window.location.host + '/connection/stream',
                    {
                        method: 'POST',
                        headers: new Headers({
                            'accept': 'application/json',
                            'content-type': 'application/json'
                        }),
                        mode: 'same-origin',
                        signal: abortController.signal,
                        body: JSON.stringify({})
                    }
                );

                eventTarget.addEventListener('open', (e) => {
                    numFailures = 0;
                    console.log(e);
                });

                eventTarget.addEventListener('message', (e) => {
                    if (e.data === null) {
                        drawText("--> ping");
                        return;
                    }
                    console.log(e);
                    processData(e.data);
                });

                function closeTransport() {
                    numFailures++
                    abortController.abort();
                    if (!reconnect) {
                        return;
                    }
                    setTimeout(function () {
                        drawText("🙏🏾 stream: connection reconnecting");
                        connect();
                    }, Math.min(numFailures * 1000, 20000));
                }

                eventTarget.addEventListener('error', (e) => {
                    console.log(e);
                    closeTransport();
                });

                eventTarget.addEventListener('close', (e) => {
                    console.log(e);
                    closeTransport();
                });
            }

            connect();

            function processData(data) {
                drawText("--> " + JSON.stringify(data));
                const pushType = data.type || 0;
                switch (pushType) {
                    case 0:
                        drawText("✨ new data from a channel " + data.channel + ": " + JSON.stringify(data.data));
                        break;
                    case 6:
                        clientID = data.data.client;
                        let subscriptions = [];
                        const subs = data.data.subs;
                        if (subs) {
                            for (const m in subs) {
                                if (subs.hasOwnProperty(m)) {
                                    subscriptions.push(m);
                                }
                            }
                        }
                        drawText("🟢 connected with client ID " + clientID + " and subscriptions: " + JSON.stringify(subscriptions));
                        break;
                    case 7:
                        clientID = null;
                        if (!data.data.reconnect) {
                            reconnect = false;
                            drawText("🔴 disconnected from a server, won't reconnect");
                        } else {
                            drawText("🔴 disconnected from a server, will reconnect");
                        }
                        break;
                    case 3:
                        drawText("🔓 unsubscribed from a channel " + data.channel);
                        break;
                    case 5:
                        drawText("🔒 subscribed to a channel " + data.channel);
                        break;
                    default:
                        drawText("😟 unsupported push type " + data.type);
                }
            }

            const container = document.getElementById('messages');

            function drawText(text) {
                let e = document.createElement('li');
                e.innerHTML = [(new Date()).toString(), ' ' + text].join(':');
                container.insertBefore(e, container.firstChild);
            }

            window.addEventListener('click', function () {
                // Every click changes status of subscription fetching backend endpoints
                // with current client ID.
                if (unsubscribed) {
                    fetch('subscribe?client=' + clientID);
                } else {
                    fetch('unsubscribe?client=' + clientID);
                }
                unsubscribed = !unsubscribed;
            });
        });
    </script>
</head>
<body>
<ul id="messages"></ul>
</body>
</html>
